package music.player

import java.io.File
import javax.sound.sampled.AudioFormat
import javax.sound.sampled.AudioInputStream
import javax.sound.sampled.AudioSystem
import javax.sound.sampled.DataLine
import javax.sound.sampled.LineUnavailableException
import javax.sound.sampled.SourceDataLine
import javax.sound.sampled.FloatControl
import java.io.BufferedInputStream
import java.io.FileInputStream
import javax.sound.sampled.AudioFileFormat
import java.io.InputStream
import music.Formats
import music.player.Effects.Reverb
import scala.actors._
import scala.actors.Actor
import scala.actors.Actor._
import scala.actors.remote._
import scala.actors.remote.RemoteActor._
import music.Messages._
import music.TimeCommands.{Command, StopCommand, FadeCommand, JumpCommand, VolumeCommand}
import scala.concurrent.ops._




object SoundPlayer extends Actor{

  var soundFile:File = null;
  var audioStream:AudioInputStream = null;
  var audioFormat:AudioFormat = Formats.WAV;
  var toSpeakers:SourceDataLine = null;  // aka sourceLine
 
  var RVB = Reverb(.125, .5, false);
  
  var playhead:Int = 0;
  
   /** Stores the status of the music player.  Valid states:
   *  'READY   - Nothing is paused, ready to go
   *  'PLAYING - Currently playing a song
   *  'PAUSED - Song is paused.
   */
  var playerStatus:Symbol = 'READY;
  
  var currentLength:Int = 0;		// In Frames
  var currentStart:Int = 0;		// In Frames
  var currentName:String = "None";
  var currentTimeCommands:Map[Int, Command] = Map();
  var currentCommand:Command = null;

  
  def act() {
    print("Music Player is ready...")

    register('Player, self);
    this.setup();

    //TODO(2) Lookup loop/react and write my own so playing music doesn't require a spawned process. (make state changes thread-safe)
    // Get Messages, Handle them.
    loop {
      react {
        case M_PLAY_SONG(file:File) => playSong(file);
        case M_PAUSE_SONG => pauseSong();
        case M_STOP_SONG => stopSong();
        case M_RESUME_SONG => resumeSong();
        case M_SEEK_SONG_PERCENT(p:Double) => seekToPercent(p);
        case M_SEEK_SONG_S(s:Double) => seekToSecond(s);
        case M_SONG_FINISHED => println("Song is done.");
        case M_SET_VOLUME(v:Double) => setVolume(v);
        case default => println("unknown incoming message to player");
      }
    }
  }
  
  def setup():Unit = { }
  
  def executeCommand(c:Command):Unit = {
    println(bytesToSeconds(playhead) + "s - " + c);
    c match {
      case x:JumpCommand => this.playhead = secondsToBytes(x.jumpToSecond);
      case StopCommand => this.ready; 
      case x:VolumeCommand => this.setVolume(x.vol);
      case x:FadeCommand => this.setupFade(x.endVol, x.durationSecs);
      case default => println("strange command recieved:" + c);
    }
  }
  
  //TODO(9) Code this
  def setVolume(v:Double):Unit = {
     // Gain goes from 6.7 to -80 ... WTF?  Probably need to establish these numbers on a per-output basis and use a
     // function to do 0-1 percentage.
//     val volume:FloatControl = toSpeakers.getControl(FloatControl.Type.MASTER_GAIN).asInstanceOf[FloatControl];
     //volume.setValue((-5).toFloat);
  }
  
  def setupFade(endVolume:Double, durationSecs:Double):Unit = {
    
  }
  
  
  /** Used stop the player and transition to the next song.  Player wont play while ready. */
  def ready():Unit = {
    this.playerStatus = 'READY;
    this.clearCurrent;
  }
  
  /** Used to indicate that the playing should stop and remain stopped. */
  def stopSong():Unit = {
    this.playerStatus = 'STOPPED;
    this.clearCurrent;
  }
  def clearCurrent():Unit = {
    this.currentName = "None";
    this.currentLength = 0;
    this.currentTimeCommands = Map();
    this.currentStart = 0;
    if(toSpeakers.isOpen())
      toSpeakers.close();
    println(this.playerStatus);
  }
  
  def pauseSong():Unit = {
    this.playerStatus = 'PAUSED;
    println(this.playerStatus);
  }
  
  def resumeSong():Unit = {
    if(this.isPaused) 
      this.playerStatus = 'PLAYING;
    println(this.playerStatus);
  }
  
  
  private def framesToSeconds(frames:Int):Double = frames.toDouble / audioFormat.getFrameRate(); 
  private def framesToBytes(frames:Int):Int = frames * audioFormat.getFrameSize();
  private def secondsToFrames(seconds:Double):Int = (seconds * audioFormat.getFrameRate()).toInt;
  private def secondsToBytes(seconds:Double):Int = framesToBytes(secondsToFrames(seconds)); // always go through secondsToFrames so you don't end up mid-sample!
  private def bytesToFrames(bytes:Int):Int = bytes / audioFormat.getFrameSize();
  private def bytesToSeconds(bytes:Int):Double = framesToSeconds(bytesToFrames(bytes));

  

  def seekToFrame(frames:Int):Unit = {
    this.seekToSecond(framesToSeconds(frames));
  }
  
  def seekToSecond(seconds:Double):Unit = {
    val length = framesToSeconds(currentLength);
    println("SEEKING TO:" + secondsToFrames(seconds) + "/" + currentLength + " = " + seconds + "/" + length + "s = " + 100*seconds/length + "%");
    this.currentCommand = new JumpCommand(seconds);
  }
  
  def seekToPercent(x:Double):Unit = {
    val mult:Double = x / 100.0;					// Percentage for multiply
    val frame:Int = (mult * currentLength).toInt;
    this.seekToFrame(frame);
  }
  
  def isReady:Boolean = this.playerStatus == 'READY;
  def isPaused:Boolean = this.playerStatus == 'PAUSED;
  def isPlaying:Boolean = this.playerStatus == 'PLAYING;
  def isStopped:Boolean = this.playerStatus == 'STOPPED;
  
  def getPlayheadTime:Double = framesToSeconds(getPlayheadFrame);
  def getPlayheadFrame:Int = bytesToFrames(playhead);
    
  
  def playSong(soundFile:File):Unit = playSong(soundFile, 0, List((10,new JumpCommand(15.0)),(20, StopCommand )));  
  
  /**
   * The method that does it all.  Note: commands is a list of (seconds,command) pairs, frames are calculated inside.
   */
   def playSong(soundFile:File, startSeconds:Double, commands:List[(Double, Command)]):Unit = {

     // val inputFileFormat:AudioFileFormat = AudioSystem.getAudioFileFormat(soundFile);
     
     // Hand off input file to the converter which returns our array of sound samples.
     val is:InputStream = new BufferedInputStream(new FileInputStream(soundFile))
     val bytes:Array[Byte] = AudioConverter.getAudioDataBytes(is, Formats.WAV);
     this.currentLength = bytesToFrames(bytes.length);
     
     // Setup the output and start up the speakers
     this.setupOutput();
     toSpeakers.start();
     
     this.playerStatus = 'PLAYING;

     
     
     // Reset player
     playhead = secondsToBytes(startSeconds);
     RVB.clearReverb(audioFormat);
     
     currentTimeCommands = commands.map(a =>(secondsToFrames(a._1), a._2)).toMap[Int,Command];
     
     spawn {
	   // Play the samples
       val frameLength:Int = audioFormat.getFrameSize();     
	   while(playhead + frameLength < bytes.length && ! this.isStopped && ! this.isReady) { 
	     // Check for jump commands (and anything else that meddles with playhead)
	     if(currentCommand != null) {
	       executeCommand(currentCommand);
	       currentCommand = null;
	     }
	     
	     // Get the sample and write it to the speakers
	     val sample:Sample = Sample(bytes, playhead, playhead + frameLength);
	     
	     playhead = playhead + frameLength;
	     toSpeakers.write(RVB(sample).toArray, 0, frameLength);  // This blocks when the buffer is full (since it's 8k, it's pretty fast)
	     
	     // Check to see if we should trigger any time based commands.
	     if(currentTimeCommands.contains(bytesToFrames(playhead))) {
	       executeCommand(this.currentTimeCommands(bytesToFrames(playhead)));     
	     }
	     
	     while(this.isPaused) { Thread.sleep(100); }
	   }
	     
	   // Clean out the buffers and close up shop.
	   toSpeakers.drain();  // Blocks 'till the buffer is empty
	   toSpeakers.close();
	   
	   // Let the player know the thread is done.
	   if(this.isPlaying) this.ready; // If we were playing, indicate we're ready for continuation
	   SoundPlayer ! M_SONG_FINISHED;
     }
  }
   
  def setupOutput():Unit = {
    // Setup the output to speakers.
    val info:DataLine.Info = new DataLine.Info(classOf[SourceDataLine], audioFormat)
    try {
      toSpeakers = AudioSystem.getLine(info).asInstanceOf[SourceDataLine];
      toSpeakers.open(audioFormat);
    } catch {
      case e:LineUnavailableException => e.printStackTrace(); System.exit(1);
      case e:Exception => e.printStackTrace(); System.exit(1);
    }
  }
  
}